// <copyright file="MembershipProviderAuthorisationPolicy.cs" company="SharpSTS">
// Copyright (c) 2007, 2008 All Right Reserved, http://sharpsts.com/
//
// This source is subject to the Microsoft Permissive License.
// Please see the License.txt file for more information.
// All other rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// </copyright>
// <author>Barry Dorrans</author>
// <email>barryd@idunno.org</email>
// <date>2008-06-15</date>
// <summary>Provides a authorisation provider provider which uses the ASP.NET membership provider as its data source.</summary>

namespace SharpSTS.SampleProviders
{
    using System;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.IdentityModel.Claims;
    using System.ServiceModel;
    using System.Web.Security;

    using SharpSTS;
    using Configuration;

    /// <summary>
    /// Provides a authorisation provider provider which uses the ASP.NET membership provider as its data source.
    /// </summary>
    /// <remarks>
    /// This is a demonstration authorisdation policy provider. As the membership provider does not give scope to save
    /// any additional information and thus provider seperation between card identifiers and the unique key for a user
    /// it does not represent an ideal provider and should not be used for a template or in a live service.
    /// </remarks>
    [System.Diagnostics.CodeAnalysis.SuppressMessage(
        "Microsoft.Performance", 
        "CA1812:AvoidUninstantiatedInternalClasses",
        Justification = "This is a provider object, instantiated by an STS.")]
    internal class MembershipProviderAuthorisationPolicy : AuthorisationPolicyProvider
    {
        /// <summary>
        /// Provides the claim information for the listed claim and assignes the specified rights to those claims.
        /// </summary>
        /// <param name="userId">The user identifier for the current request.</param>
        /// <param name="relyingParty">The <see cref="EndpointAddress"/> of the relying party that is requesting the claims.</param>
        /// <param name="claimTypes">A collection of claim types to provide information for.</param>
        /// <param name="rights">The rights to assign to this claim.</param>
        /// <returns>A list of claims.</returns>
        /// <exception cref="ArgumentNullException">Thrown if cardId, rights or claimType is null.</exception>
        /// <exception cref="ArgumentException">Thrown if cardVersion is not valid or cardId does not contain a membership guid.</exception>
        /// <remarks>The membership provider only supports one claims, email address.</remarks>
        public override Collection<Claim> GetClaims(
            object userId, 
            EndpointAddress relyingParty, 
            ReadOnlyCollection<string> claimTypes, 
            string rights)
        {
            if (null == userId)
            {
                throw new ArgumentNullException("userId");
            }

            if (string.IsNullOrEmpty(rights))
            {
                throw new ArgumentNullException("rights");
            }

            if (null == claimTypes || claimTypes.Count == 0)
            {
                throw new ArgumentNullException("claimTypes");
            }

            string membershipKey = userId.ToString();

            MembershipUser userDetails = Membership.GetUser(new Guid(membershipKey), true);

            if (null == userDetails)
            {
                throw new ArgumentException("Does not resolve to a user.", "userId");
            }

            Collection<Claim> filledClaims = new Collection<Claim>();
            foreach (string claimURI in claimTypes)
            {
                if (claimURI == ClaimTypes.Email)
                {
                    filledClaims.Add(new Claim(claimURI, userDetails.Email, rights));
                }
            }

            return filledClaims;
        }

        /// <summary>
        /// Validates the association between the authenicated user and the card id used during the request.
        /// </summary>
        /// <param name="userName">User name sent during authentication.</param>
        /// <param name="cardId">Card identity of the card used to send the request.</param>
        /// <param name="cardVersion">The version number of the card presented.</param>
        /// <returns>
        /// True if the username is authorised to use the card, otherwise false.
        /// </returns>
        /// <exception cref="ArgumentNullException">Thrown if userName or cardId is null.</exception>
        /// <exception cref="ArgumentException">Thrown if cardVersion is not valid or cardId does not contain a membership guid.</exception>
        /// <remarks>Override this method to provide your own authorisation process.</remarks>
        public override bool AuthorisedToUseCardId(string userName, string cardId, int cardVersion)
        {
            if (string.IsNullOrEmpty(userName))
            {
                throw new ArgumentNullException("userName");
            }

            if (string.IsNullOrEmpty(cardId))
            {
                throw new ArgumentNullException("cardId");
            }

            if (cardId.IndexOf('/') == -1)
            {
                throw new ArgumentException("Incorrect format", "cardId");
            }

            if (cardId.LastIndexOf('/') == cardId.Length - 1)
            {
                throw new ArgumentException("Missing the membership guid.", "cardId");
            }

            if (cardVersion < 1)
            {
                throw new ArgumentException("Cannot be less than zero.", "cardVersion");
            }

            MembershipUser userDetails = Membership.GetUser(userName);
            
            if (null == userDetails)
            {
                return false;
            }

            string cardUniqueId = cardId.Substring(cardId.LastIndexOf('/') + 1);
            return userDetails.ProviderUserKey.ToString() == cardUniqueId;
        }

        /// <summary>
        /// Validates the association between the PPID for a self-issued backed card
        /// and the card id used during the request.
        /// </summary>
        /// <param name="privatePersonalIdentifier">PPID sent during authentication.</param>
        /// <param name="issuerPublicKeyHash">A hash of the public key of the issuer for the PPID claim.</param>
        /// <param name="cardId">Card identity of the card used to send the request.</param>
        /// <param name="cardVersion">The version number of the card presented.</param>
        /// <returns>
        /// True if the username is authorised to use the card, otherwise false.
        /// </returns>
        public override bool AuthorisedToUseCardId(Claim privatePersonalIdentifier, string issuerPublicKeyHash, string cardId, int cardVersion)
        {
            // TODO: Return false once we have a full blown profile provider.
            return true;
        }

        /// <summary>
        /// Decides if the information card used requires a refresh.
        /// </summary>
        /// <param name="cardId">The card identifier.</param>
        /// <param name="cardVersion">The card version.</param>
        /// <returns>True if a card refresh is required, otherwise false.</returns>
        public override bool IsCardRefreshRequired(string cardId, int cardVersion)
        {
            return false;
        }

        /// <summary>
        /// Returns a card identifier for the supplied username, card type and card version number.
        /// </summary>
        /// <param name="userId">The user identifier to generation the card identifier for.</param>
        /// <param name="authenticationType">The type of the authentication used for the card.</param>
        /// <returns>An identifier for an information card.</returns>
        public override string GetCardIdForUserId(object userId, AuthenticationType authenticationType)
        {
            MembershipUserCollection userDetails = Membership.FindUsersByName(userId.ToString());
            if (userDetails.Count == 0)
            {
                throw new ArgumentException("User not found.", "userId");
            }

            if (userDetails.Count > 1)
            {
                throw new ArgumentException("Duplicate userIDs found in database.", "userId");
            }

            return string.Format(
                CultureInfo.InvariantCulture, 
                "urn:sharpsts:{0}:{1}/{2}/{3}",
                STSSettings.Settings.Issuer, 
                userId,
                authenticationType,
                userDetails[userId.ToString()].ProviderUserKey);
        }

        /// <summary>
        /// Returns a user identifier that the managed information card belongs to.
        /// </summary>
        /// <param name="cardId"> The card identifier.</param>
        /// <param name="authenticationType">The type of the authentication used for the card.</param>
        /// <returns>The user identifier the card belongs to.</returns>
        public override object GetUserIdFromCardId(string cardId, AuthenticationType authenticationType)
        {
            if (string.IsNullOrEmpty(cardId))
            {
                throw new ArgumentNullException("cardId");
            }

            if (cardId.IndexOf('/') == -1)
            {
                throw new ArgumentException("Incorrect format", "cardId");
            }

            if (cardId.LastIndexOf('/') == cardId.Length - 1)
            {
                throw new ArgumentException("Missing the membership guid.", "cardId");
            }

            return (cardId.Substring(cardId.LastIndexOf('/') + 1));
        }

        /// <summary>
        /// Authenticates a username and password
        /// </summary>
        /// <param name="userName">The username to authenticate.</param>
        /// <param name="password">The password to authenticate.</param>
        /// <returns>True if athenticated, otherwise false.</returns>
        public override bool AuthenticateUserNamePassword(string userName, string password)
        {
            return Membership.ValidateUser(userName, password);
        }
    }
}
